﻿using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using Sparklings.JsonUtils;
using Sparklings.LevelElements;
using Sparklings.RayTracing;
using Sparklings.Rendering;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Sparklings
{
    internal class LevelLoadingState
    {
        public LevelLoadingState(int width, int height)
        {
            this.width = width;
            this.height = height;
        }
        public void NextTile()
        {
            ++curTile;
        }
        public int currentTileX { get { return curTile / height; } }
        public int currentTileY { get { return curTile % height; } }
        private int width, height, curTile;
    }
    public interface ILevelNeededData
    {
        ContentManager Content { get; }
        GraphicsDevice GraphicsDevice { get; }
        Random RNG { get; }
    }
    public class LevelNeededData : ILevelNeededData
    {
        public ContentManager Content { get; set; }
        public GraphicsDevice GraphicsDevice { get; set; }
        public Random RNG { get; set; }
    }
    public class Level
    {
        public Level(ILevelNeededData g, int width, int height)
        {
            if (g == null) { throw new ArgumentNullException("g"); }
            ContentManager = g.Content;
            GraphicsDevice = g.GraphicsDevice;
            Tiles = new Tile[width, height];
            for (int i = 0; i < width; ++i)
            {
                for (int j = 0; j < height; ++j)
                {
                    if (i == 1 && j == 1)
                    {
                        Tiles[i, j] = new Spawner(this, i, j, 2, RayColor.Green);
                    }
                    else if (i == width - 1 && j == 1)
                    {
                        Tiles[i, j] = new ExitPoint(this, i, j, RayColor.None);
                    }
                    else
                    {
                        if (j >= height / 2)
                        {
                            Tiles[i, j] = new DefaultPlatform(this, i, j);
                        }
                        else
                        {
                            Tiles[i, j] = new EmptyTile(this, i, j);
                        }
                    }
                }
            }
            Sparks = new List<Sparkling>();
            SparklingsNeeded = 2;
            InitRenderContext();
            LoadGraphicsRelatedElements();
        }
        private Level(ILevelNeededData g)
        {
            if (g == null) { throw new ArgumentNullException("g"); }
            ContentManager = g.Content;
            GraphicsDevice = g.GraphicsDevice;
            Sparks = new List<Sparkling>();
        }

        public void LoadGraphicsRelatedElements()
        {
            Font = ContentManager.Load<SpriteFont>("DefaultFont");
            CombineBaseLight = ContentManager.Load<Effect>("Shader");
            Spritebatch = new SpriteBatch(GraphicsDevice);
            foreach (var t in Tiles)
            {
                t.LoadGraphicsRelatedElements();
            }
            foreach (var s in Sparks)
            {
                s.LoadGraphicsRelatedElements();
            }
            RenderContext.LoadGraphicsRelatedElements();
        }

        public void Update(GameTime t, Vector2? mousePosIn = null, bool? cursorDown = null)
        {
            HandleUserInput(t, mousePosIn, cursorDown);
            var l = ListLightSurfaces();
            PrepareRayHits(t);
            foreach (Spot spot in Spots)
            {
                foreach (ColoredRay ray in spot.Rays)
                {
                    ray.Apply(l);
                }
            }
            foreach (Tile tile in Tiles)
            {
                tile.Update(t);
            }
            foreach (var spark in Sparks)
            {
                spark.Update(t);
            }
            Sparks.RemoveAll(p => p.Disappeared);
        }
        protected void PrepareRayHits(GameTime t)
        {
            foreach (Tile tile in Tiles)
            {
                tile.PrepareRayHits();
            }
            foreach (Sparkling s in Sparks)
            {
                s.PrepareRayHits(t);
            }
        }
        protected void HandleUserInput(GameTime t, Vector2? mousePosIn = null, bool? cursorDown = null)
        {
            Vector2 mousePos;
            if (mousePosIn != null)
            {
                mousePos = RenderContext.FromScreenPosition((Vector2)mousePosIn);
            }
            else
            {
                mousePos = RenderContext.FromScreenPosition(new Vector2(Mouse.GetState().X, Mouse.GetState().Y));
            }
            bool isCursorDown = cursorDown ?? Mouse.GetState().LeftButton == ButtonState.Pressed;

            if (isCursorDown)
            {
                if (!MouseWasDown)
                {
                    MouseWasDown = true;
                    if (IsInLevel((int)mousePos.X, (int)mousePos.Y))
                    {
                        var e = Tiles[(int)mousePos.X, (int)mousePos.Y] as IActivable;
                        if (e != null)
                        {
                            e.Activate();
                        }
                    }
                }
            }
            else
            {
                MouseWasDown = false;
            }
        }

        protected void InitRenderContext()
        {
            float renderRatio = _renderBounds.X / _renderBounds.Y;
            float tilesRatio = ((float)Width) / Height;
            if (renderRatio > tilesRatio)
            {

                _renderBounds.X *= tilesRatio / renderRatio;
            }
            else
            {
                _renderBounds.Y /= tilesRatio / renderRatio;
            }

            RenderContext = new RenderContext(
                GraphicsDevice,
                ContentManager,
                renderTargetPixelSize: _renderBounds,
                dimensionsInTiles: new Vector2(Width, Height));
        }

        protected Texture2D DrawBase()
        {
            var baseRenderTarget = new RenderTarget2D(GraphicsDevice, (int)RenderContext.RenderTargetPixelSize.X, (int)RenderContext.RenderTargetPixelSize.Y);
            GraphicsDevice.SetRenderTarget(baseRenderTarget);
            GraphicsDevice.Clear(Color.FromNonPremultiplied(140, 150, 150, 255));
            GraphicsDevice.BlendState = BlendState.AlphaBlend;
            foreach (IRenderable d in Drawables)
            {
                d.DrawBase(RenderContext);
            }
            GraphicsDevice.SetRenderTarget(null);
            return baseRenderTarget;
        }
        protected Texture2D DrawLight()
        {
            int ambientLight = 128;
            var lightRenderTarget = new RenderTarget2D(GraphicsDevice, (int)RenderContext.RenderTargetPixelSize.X, (int)RenderContext.RenderTargetPixelSize.Y);
            GraphicsDevice.SetRenderTarget(lightRenderTarget);
            {
                GraphicsDevice.Clear(Color.FromNonPremultiplied(ambientLight, ambientLight, ambientLight, 255));
                GraphicsDevice.BlendState = BlendState.Additive;
                foreach (IRenderable d in Drawables)
                {
                    d.DrawLight(RenderContext);
                }
                GraphicsDevice.BlendState = BlendState.AlphaBlend;
            }
            GraphicsDevice.SetRenderTarget(null);
            return lightRenderTarget;
        }
        protected void DrawSpotLights()
        {
            foreach (IRenderable d in Drawables)
            {
                d.DrawEmissive(RenderContext);
            }
        }
        protected void DrawParticles()
        {
            GraphicsDevice.BlendState = BlendState.Additive;
            foreach (IRenderable d in Drawables)
            {
                d.DrawAdditive(RenderContext);
            }
            GraphicsDevice.BlendState = BlendState.AlphaBlend;
        }
        protected void DrawText()
        {
            Spritebatch.Begin(SpriteSortMode.BackToFront, BlendState.AlphaBlend);
            string s;
            if (SparklingsNeeded == 0)
            {
                s = "";
            }
            else if (SparklingsNeeded == 1)
            {
                s = "Only one sparkling needed to finish";
            }
            else
            {
                s = String.Format("Still need {0} spaklings to finish", SparklingsNeeded);
            }
            Spritebatch.DrawString(Font, s, new Vector2(0, 0), Color.White);
            if (Finished)
            {
                string levelFinished = "Level finished !";
                Spritebatch.DrawString(Font, levelFinished, RenderContext.RenderTargetPixelSize / 2 - Font.MeasureString(levelFinished) / 2, Color.White);
            }
            foreach (var t in Tiles)
            {
                t.DrawText(RenderContext, Spritebatch);
            }
            Spritebatch.End();
        }
        public Texture2D GetDrawn(GraphicsDevice g)
        {
            var finalRenderTarget = new RenderTarget2D(GraphicsDevice, (int)RenderContext.RenderTargetPixelSize.X, (int)RenderContext.RenderTargetPixelSize.Y);
            Texture2D baseRenderTarget = DrawBase();

            Texture2D lightRenderTarget = DrawLight();
            GraphicsDevice.SetRenderTarget(finalRenderTarget);
            {
                GraphicsDevice.Textures[1] = baseRenderTarget;
                GraphicsDevice.Textures[2] = lightRenderTarget;
                GraphicsDevice.Clear(Color.Pink);
                CombineBaseLight.CurrentTechnique = CombineBaseLight.Techniques["BlendLightScene"];
                Spritebatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, null, null, null, CombineBaseLight);
                Spritebatch.Draw(baseRenderTarget, Vector2.Zero, Color.White);
                Spritebatch.End();
                DrawSpotLights();
                DrawParticles();
                DrawText();
            }
            GraphicsDevice.SetRenderTarget(null);
            return finalRenderTarget;
        }
        protected IEnumerable<IRenderable> Drawables { get { return Tiles.Cast<IRenderable>().Concat(Sparks.Cast<IRenderable>()); } }

        public static JToken ConvertToJson(JsonOverrider converter, object value)
        {
            var tok = new JObject();
            var l = value as Level;
            tok["name"] = converter.Write(l.Name);
            tok["helptext"] = converter.Write(l.HelpText);
            tok["width"] = converter.Write(l.Width);
            tok["height"] = converter.Write(l.Height);
            tok["tiles"] = converter.Write(l.Tiles);
            tok["sparklingsneeded"] = converter.Write(l.SparklingsNeeded);
            return tok;
        }
        public static object ConvertFromJson(JsonOverrider converter, JToken toConvert)
        {
            Level level = new Level(converter.GetClosestParentWithType<ILevelNeededData>());
            level.Name = converter.Read<string>(toConvert["name"], level);
            if (toConvert["helptext"] != null)
            {
                level.HelpText = converter.Read<string>(toConvert["helptext"], level);
            }
            int width = converter.Read<int>(toConvert["width"], level);
            int height = converter.Read<int>(toConvert["height"], level);
            level.LoadingState = new LevelLoadingState(width, height);
            level.Tiles = converter.Read<Tile[,]>(toConvert["tiles"], level);
            level.SparklingsNeeded = converter.Read<int>(toConvert["sparklingsneeded"], level);
            level.InitRenderContext();
            level.LoadGraphicsRelatedElements();
            return level;
        }
        public string Save()
        {
            var converter = new OverridingJsonConverter();
            converter.addToConvertMethods(typeof(Level), ConvertFromJson, ConvertToJson);
            converter.addToConvertMethods(typeof(Tile), Tile.ConvertFromJson, Tile.ConvertToJson);
            return JsonConvert.SerializeObject(this,
                new JsonSerializerSettings
                    {
                        Formatting = Newtonsoft.Json.Formatting.Indented,
                        Converters = new JsonConverter[] { converter, new StringEnumConverter() },
                        NullValueHandling = NullValueHandling.Include
                    });
        }
        public static Level Load(ILevelNeededData g, string s)
        {
            try
            {
                var converter = new OverridingJsonConverter(g);
                converter.addToConvertMethods(typeof(Level), ConvertFromJson, ConvertToJson);
                converter.addToConvertMethods(typeof(Tile), Tile.ConvertFromJson, Tile.ConvertToJson);
                return JsonConvert.DeserializeObject<Level>(s,
                    new JsonSerializerSettings
                    {
                        Converters = new JsonConverter[] { converter }
                    });
            }
            catch (Exception e)
            {
                throw new Exception("Level loading failed.", e);
            }
        }

        public bool IsReachableBySparkling(Sparkling s, int x, int y)
        {
            if (x < 0 || y < 0 || x >= Tiles.GetLength(0) || y >= Tiles.GetLength(1))
            {
                return false;
            }
            return Tiles[x, y].IsPassableBySparkling(s);
        }
        public bool IsInLevel(int x, int y)
        {
            return x < Width && 0 <= x && y < Height && 0 <= y;
        }
        protected IList<LightHittable> ListLightSurfaces()
        {
            var ret = new List<LightHittable>();
            for (int i = 0; i < Width; ++i)
            {
                for (int j = 0; j < Height; ++j)
                {
                    ret.AddRange(Tiles[i, j].LightSurfaces);
                }
            }
            foreach (var sparkling in Sparks)
            {
                ret.AddRange(sparkling.LightSurfaces);
            }
            return ret;
        }

        internal LevelLoadingState LoadingState = null;
        public int Width { get { return Tiles.GetLength(0); } }
        public int Height { get { return Tiles.GetLength(1); } }
        public Tile[,] Tiles;
        public List<Spot> Spots
        {
            get
            {
                List<Spot> ret = new List<Spot>();
                foreach (Tile t in Tiles)
                {
                    var spotPlateform = t as SpotPlateform;
                    if (spotPlateform != null)
                    {
                        ret.Add(spotPlateform.spot);
                    }
                }
                return ret;
            }
        }
        public List<Sparkling> Sparks;
        public int SparklingsNeeded
        {
            get { return sparklingsNeeded; }
            set { sparklingsNeeded = value < 0 ? 0 : value; }
        }
        private int sparklingsNeeded;
        public bool Finished { get { return SparklingsNeeded == 0; } }
        public int StarsLeft
        {
            get
            {
                int c = 0;
                foreach (Tile t in Tiles)
                {
                    StarTile starTile = t as StarTile;
                    if (starTile != null && starTile.Collected == false)
                    {
                        ++c;
                    }
                }
                return c;
            }
        }
        public int StarsCount
        {
            get
            {
                int c = 0;
                foreach (Tile t in Tiles)
                {
                    if ((t as StarTile) != null)
                    {
                        ++c;
                    }
                }
                return c;
            }
        }
        public int StarsTaken { get { return StarsCount - StarsLeft; } }
        public ContentManager ContentManager;
        public GraphicsDevice GraphicsDevice;
        public SpriteBatch Spritebatch;
        public static Random StaticRNG = new Random();
        public RenderContext RenderContext { get; private set; }
        /// <summary>
        /// The level will render itself on a texture of this size.
        /// When set, if the ratio is bad, it will correct it to dimensions,
        /// that will be stricly included in those given.
        /// </summary>
        public Vector2 RenderBounds
        {
            get
            {
                return _renderBounds;
            }
            set
            {
                _renderBounds = value;
                InitRenderContext();
            }
        }
        private Vector2 _renderBounds = new Vector2(500, 400);
        public string Name = "";
        public string HelpText = "";
        public SpriteFont Font;
        private Effect CombineBaseLight;
        private bool MouseWasDown = false;
    }
}
